package math.base;

import java.util.Arrays;

/**
 * @author Saga
 *
 */
public class Matrix2d {
    public Matrix2d() {
        this.elements = new double[]{
                1.0, 0.0,
                0.0, 1.0 };
    }

    public Matrix2d(final double _a11, final double _a12, final double _a21, final double _a22) {
        this.elements = new double[]{ _a11, _a21, _a12, _a22 };
    }

    public Matrix2d(final Coord2d _col1, final Coord2d _col2) {
        this.elements = new double[]{ _col1.getX(), _col1.getY(), _col2.getX(), _col2.getY() };
    }

    //coord
    public final void setCoord(final int _rowId, final int _colId, final double _val) {
        switch (_rowId) {
        case 0: {
            switch (_colId) {
            case 0:
                this.elements[0] = _val;
                break;

            case 1:
                this.elements[2] = _val;
                break;

            default:
                throw new IndexOutOfBoundsException("");
            }
        }
        break;

        case 1: {
            switch (_colId) {
            case 0:
                this.elements[1] = _val;
                break;

            case 1:
                this.elements[3] = _val;
                break;

            default:
                throw new IndexOutOfBoundsException("");
            }
        }
        break;

        default:
            throw new IndexOutOfBoundsException("");
        }
    }

    public final double getCoord(final int _rowId, final int _colId) {
        switch (_rowId) {
        case 0: {
            switch (_colId) {
            case 0:
                return this.elements[0];

            case 1:
                return this.elements[2];

            default:
                throw new IndexOutOfBoundsException("");
            }
        }

        case 1: {
            switch (_colId) {
            case 0:
                return this.elements[1];

            case 1:
                return this.elements[3];

            default:
                throw new IndexOutOfBoundsException("");
            }
        }

        default:
            throw new IndexOutOfBoundsException("");
        }
    }

    public final void setRow(final int _id, final Coord2d _row) {
        if (_id < 0 || _id > 1) {
            throw new IndexOutOfBoundsException("");
        }

        this.elements[_id] = _row.getX();
        this.elements[_id + 2] = _row.getY();
    }

    public final Coord2d getRow(final int _id) {
        if (_id < 0 || _id > 1) {
            throw new IndexOutOfBoundsException("");
        }

        return new Coord2d(this.elements[_id], this.elements[_id + 2]);
    }

    public final void setColumn(final int _id, final Coord2d _col) {
        if (_id < 0 || _id > 1) {
            throw new IndexOutOfBoundsException("");
        }

        final int pos = _id * 2;
        this.elements[pos] = _col.getX();
        this.elements[pos + 1] = _col.getY();
    }

    public final Coord2d getColumn(final int _id) {
        if (_id < 0 || _id > 1) {
            throw new IndexOutOfBoundsException("");
        }

        final int pos = _id * 2;
        return new Coord2d(this.elements[pos], this.elements[pos + 1]);
    }

    public final void setIdentity() {
        this.elements[0] = 1.0;
        this.elements[1] = 0.0;
        this.elements[2] = 0.0;
        this.elements[3] = 1.0;
    }

    public static final Matrix2d createFromIdentity() {
        return new Matrix2d(1.0, 0.0, 0.0, 1.0);
    }

    public final void setZero() {
        Arrays.fill(this.elements, 0.0);
    }

    public static final Matrix2d createFromZero() {
        return new Matrix2d(0.0, 0.0, 0.0, 0.0);
    }

    public final void setConstant(final double _val) {
        Arrays.fill(this.elements, _val);
    }

    public static final Matrix2d createFromConstant(final double _val) {
        return new Matrix2d(_val, _val, _val, _val);
    }

    public final void setDiagonal(final double _val) {
        this.elements[0] = _val;
        this.elements[1] = 0.0;
        this.elements[2] = 0.0;
        this.elements[3] = _val;
    }

    public static final Matrix2d createFromDiagonal(final double _val) {
        return new Matrix2d(_val, 0.0, 0.0, _val);
    }

    public final void setDiagonal(final Coord2d _coord) {
        this.elements[0] = _coord.getX();
        this.elements[1] = 0.0;
        this.elements[2] = 0.0;
        this.elements[3] = _coord.getY();
    }

    public static final Matrix2d createFromDiagonal(final Coord2d _coord) {
        return new Matrix2d(_coord.getX(), 0.0, 0.0, _coord.getY());
    }

    //add
    public final void add(final Matrix2d _mat) {
        this.elements[0] += _mat.elements[0];
        this.elements[1] += _mat.elements[1];
        this.elements[2] += _mat.elements[2];
        this.elements[3] += _mat.elements[3];
    }

    public final Matrix2d added(final Matrix2d _mat) {
        return new Matrix2d(this.elements[0] + _mat.elements[0],
                this.elements[2] + _mat.elements[2], this.elements[1] + _mat.elements[1],
                this.elements[3] + _mat.elements[3]);
    }

    //subtract
    public final void subtract(final Matrix2d _mat) {
        this.elements[0] -= _mat.elements[0];
        this.elements[1] -= _mat.elements[1];
        this.elements[2] -= _mat.elements[2];
        this.elements[3] -= _mat.elements[3];
    }

    public final Matrix2d subtracted(final Matrix2d _mat) {
        return new Matrix2d(this.elements[0] - _mat.elements[0],
                this.elements[2] - _mat.elements[2], this.elements[1] - _mat.elements[1],
                this.elements[3] - _mat.elements[3]);
    }

    //reverse
    public final void reverse() {
        this.elements[0] = -this.elements[0];
        this.elements[1] = -this.elements[1];
        this.elements[2] = -this.elements[2];
        this.elements[3] = -this.elements[3];
    }

    public final Matrix2d reversed() {
        return new Matrix2d(-this.elements[0], -this.elements[2], -this.elements[1],
                -this.elements[3]);
    }

    //multiply
    public final void multiply(final double _scalar) {
        this.elements[0] *= _scalar;
        this.elements[1] *= _scalar;
        this.elements[2] *= _scalar;
        this.elements[3] *= _scalar;
    }

    public final Matrix2d multiplied(final double _scalar) {
        return new Matrix2d(this.elements[0] * _scalar, this.elements[2] * _scalar,
                this.elements[1] * _scalar, this.elements[3] * _scalar);
    }

    public final void multiply(final Matrix2d _mat) {
        final double temp11 =
                this.elements[0] * _mat.elements[0] + this.elements[2] * _mat.elements[1];
        final double temp12 =
                this.elements[0] * _mat.elements[2] + this.elements[2] * _mat.elements[3];
        final double temp21 =
                this.elements[1] * _mat.elements[0] + this.elements[3] * _mat.elements[1];
        this.elements[3] =
                this.elements[1] * _mat.elements[2] + this.elements[3] * _mat.elements[3];
        this.elements[0] = temp11;
        this.elements[1] = temp21;
        this.elements[2] = temp12;
    }

    public final Matrix2d multiplied(final Matrix2d _mat) {
        final double temp11 =
                this.elements[0] * _mat.elements[0] + this.elements[2] * _mat.elements[1];
        final double temp12 =
                this.elements[0] * _mat.elements[2] + this.elements[2] * _mat.elements[3];
        final double temp21 =
                this.elements[1] * _mat.elements[0] + this.elements[3] * _mat.elements[1];
        final double temp22 =
                this.elements[1] * _mat.elements[2] + this.elements[3] * _mat.elements[3];
        return new Matrix2d(temp11, temp12, temp21, temp22);
    }

    public final void preMultiply(final Matrix2d _mat) {
        final double temp11 =
                _mat.elements[0] * this.elements[0] + _mat.elements[2] * this.elements[1];
        final double temp12 =
                _mat.elements[0] * this.elements[2] + _mat.elements[2] * this.elements[3];
        final double temp21 =
                _mat.elements[1] * this.elements[0] + _mat.elements[3] * this.elements[1];
        this.elements[3] =
                _mat.elements[1] * this.elements[2] + _mat.elements[3] * this.elements[3];
        this.elements[0] = temp11;
        this.elements[1] = temp21;
        this.elements[2] = temp12;
    }

    public final Matrix2d preMultiplied(final Matrix2d _mat) {
        final double temp11 =
                _mat.elements[0] * this.elements[0] + _mat.elements[2] * this.elements[1];
        final double temp12 =
                _mat.elements[0] * this.elements[2] + _mat.elements[2] * this.elements[3];
        final double temp21 =
                _mat.elements[1] * this.elements[0] + _mat.elements[3] * this.elements[1];
        final double temp22 =
                _mat.elements[1] * this.elements[2] + _mat.elements[3] * this.elements[3];
        return new Matrix2d(temp11, temp12, temp21, temp22);
    }

    public final Coord2d multiplied(final Coord2d _coord) {
        final double tempX = this.elements[0] * _coord.getX() + this.elements[2] * _coord.getY();
        final double tempY = this.elements[1] * _coord.getX() + this.elements[3] * _coord.getY();
        return new Coord2d(tempX, tempY);
    }

    public final Coord2d preMultiplied(final Coord2d _coord) {
        final double tempX = _coord.getX() * this.elements[0] + _coord.getY() * this.elements[1];
        final double tempY = _coord.getX() * this.elements[2] + _coord.getY() * this.elements[3];
        return new Coord2d(tempX, tempY);
    }

    //divide
    public final void divide(final double _denom) {
        final double scalar = 1.0 / _denom;
        this.elements[0] *= scalar;
        this.elements[1] *= scalar;
        this.elements[2] *= scalar;
        this.elements[3] *= scalar;
    }

    public final Matrix2d divided(final double _denom) {
        final double scalar = 1.0 / _denom;
        return new Matrix2d(this.elements[0] * scalar, this.elements[2] * scalar,
                this.elements[1] * scalar, this.elements[3] * scalar);
    }

    //tranpose
    public final void transpose() {
        final double temp = this.elements[1];
        this.elements[1] = this.elements[2];
        this.elements[2] = temp;
    }

    public final Matrix2d transposed() {
        return new Matrix2d(this.elements[0], this.elements[1], this.elements[2], this.elements[3]);
    }

    //invert
    public final void invert() {
        final double det =
                this.elements[0] * this.elements[3] - this.elements[1] * this.elements[2];
        if (Math.abs(det) < 0.0) {
            throw new ArithmeticException("");
        }

        final double scalar = 1.0 / det;
        this.elements[0] = this.elements[3] * scalar;
        this.elements[1] = -this.elements[1] * scalar;
        this.elements[2] = -this.elements[2] * scalar;
        this.elements[0] = this.elements[0] * scalar;
    }

    public final Matrix2d inverted() {
        final double det =
                this.elements[0] * this.elements[3] - this.elements[1] * this.elements[2];
        if (Math.abs(det) < 0.0) {
            throw new ArithmeticException("");
        }

        final double scalar = 1.0 / det;
        return new Matrix2d(this.elements[3] * scalar, -this.elements[2] * scalar,
                -this.elements[1] * scalar, this.elements[0] * scalar);
    }

    public final void power(final int _order) {
        switch (_order) {
        case 1:
            return;

        case 0:
            this.setIdentity();
            return;

        case -1:
            this.invert();
            return;

        default: {
            if (_order < 0) {
                this.invert();
            }

            int tempOrder = _order;
            if (tempOrder < 0) {
                tempOrder = -tempOrder;
            }
            --tempOrder;

            final Matrix2d temp = this.clone();
            for (; ; ) {
                if (tempOrder % 2 == 1) {
                    this.multiply(temp);
                }
                if (tempOrder == 1) {
                    return;
                }
                temp.multiply(temp);
                tempOrder >>= 1;
            }
        }
        }
    }

    public final Matrix2d powered(final int _order) {
        switch (_order) {
        case 1:
            return this.clone();

        case 0:
            return createFromIdentity();

        case -1:
            return this.inverted();

        default: {
            Matrix2d result = this.clone();
            if (_order < 0) {
                result = this.inverted();
            }

            int tempOrder = _order;
            if (tempOrder < 0) {
                tempOrder = -tempOrder;
            }
            --tempOrder;

            final Matrix2d temp = result.clone();
            for (; ; ) {
                if (tempOrder % 2 == 1) {
                    result.multiply(temp);
                }
                if (tempOrder == 1) {
                    return result;
                }
                temp.multiply(temp);
                tempOrder >>= 1;
            }
        }
        }
    }

    //attribute
    public final double calcTrace() {
        return this.elements[0] + this.elements[3];
    }

    public final double calcDeterminant() {
        return this.elements[0] * this.elements[3] - this.elements[1] * this.elements[2];
    }

    @Override
    public final Matrix2d clone() {
        return new Matrix2d(this.elements[0], this.elements[2], this.elements[1],
                this.elements[3]);
    }

    private final double[] elements;
}
